﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Security;
using System.Security.Permissions;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Administration;
using Microsoft.Office.Server.UserProfiles;
using System.Diagnostics;
using Microsoft.Office.Server.Search.Administration;
using Microsoft.Office.Server.Search.Query;
using Microsoft.SharePoint.Administration;
using System.Globalization;
using System.Web.UI;

namespace MyLinksMenu
{
	public class MyLinksMenuControl : ToolBarMenuButton
	{
		const string CACHE_ID = "MyLinksMenu_SiteList";
		const int MAX_TITLE_CHARS = 35;
		const int MENUGROUPID = 1000000;

		private const string TITLE_ELLIPSIS = "...";
		private readonly string TEXT_ROOT_SITE = GetLocalizedString("MyLinks_RootSite");
		private readonly string TEXT_MY_SHAREPOINT_SITES = GetLocalizedString("MyLinks_MySharePointSites");
		private readonly string TEXT_ADD_TO_MY_LINKS = GetLocalizedString("MyLinks_AddToMyLinks");
		private readonly string TEXT_MANAGE_MY_LINKS = GetLocalizedString("MyLinks_ManageMyLinks");

		private string _Text = GetLocalizedString("MyLinks_DefaultTitle");
		private string _SearchScope = "My SharePoint Sites"; 

		private string _WebUrl;
		private UserProfile _CurrentUser;
		private bool _ShowQuickLinks = true;
		private bool _UseMemberships = true;
		private bool _UseSiteSearch = false;
		private int _SearchResultLimit = 1000; //Suprisingly, this can go much higher without dramatically affecting performance. 
		private bool _IncludeWebsInSearchResults = true;
		private bool _CacheSiteList = true;
		private int _CacheDuration = 30;
		private int _SiteMenuDepth = 1;
		private bool _LoadedSiteListFromCache = false;
		private Dictionary<string, long> _LoadTimes = new Dictionary<string, long>();
		private List<string> _ErrorMessages = new List<string>();


		/// <summary>
		/// The text that will be displayed on the menu's main button.
		/// </summary>
		public override string Text
		{
			get
			{
				return _Text;
			}
			set
			{
				_Text = value;
			}
		}

		/// <summary>
		/// The URL of the current SPWeb.
		/// </summary>
		public string WebUrl
		{
			get { return _WebUrl; }
		}

		/// <summary>
		/// Specifies whether or not to show Quick/My Links and their associated actions, e.g. "Add to My Links", etc..
		/// </summary>
		public bool ShowQuickLinks
		{
			get { return _ShowQuickLinks; }
			set { _ShowQuickLinks = value; }
		}

		/// <summary>
		/// Specifies whether or not the site list should be populated using the user's Memberships.
		/// </summary>
		public bool UseMemberships
		{
			get { return _UseMemberships; }
			set { _UseMemberships = value; }
		}

		/// <summary>
		/// Specifies whether or not the site list should be populated using search.
		/// </summary>
		public bool UseSiteSearch
		{
			get { return _UseSiteSearch; }
			set { _UseSiteSearch = value; }
		}

		/// <summary>
		/// The scope used to perform the site search.
		/// </summary>
		public string SearchScope
		{
			get { return _SearchScope; }
			set { _SearchScope = value; }
		}

		/// <summary>
		/// The maximum sites/webs returned when searching.
		/// </summary>
		public int SearchResultLimit
		{
			get { return _SearchResultLimit; }
			set
			{
				if (value < 0)
					throw new ArgumentOutOfRangeException("value", value, "The SearchResultLimit must be greater than or equal to zero.");

				_SearchResultLimit = value;
			}
		}

		/// <summary>
		/// If set to false, only site collection root webs will be listed in the site menu. The default value is true.
		/// </summary>
		public bool IncludeWebsInSearchResults
		{
			get { return _IncludeWebsInSearchResults; }
			set { _IncludeWebsInSearchResults = value; }
		}

		/// <summary>
		/// Enable/disables the use of the user's session to cache their site list.
		/// </summary>
		public bool CacheSiteList
		{
			get { return _CacheSiteList; }
			set { _CacheSiteList = true; }
		}

		/// <summary>
		/// The duration, in minutes, that the site list cache should be used.
		/// </summary>
		public int CacheDuration
		{
			get { return _CacheDuration; }
			set { _CacheDuration = value; }
		}

		/// <summary>
		/// The depth of the sub
		/// </summary>
		public int SiteMenuDepth
		{
			get { return _SiteMenuDepth; }
			set 
			{
				if (value < 0)
					throw new ArgumentOutOfRangeException("value", value, "The SiteMenuDepth must be greater than or equal to zero.");

				_SiteMenuDepth = value; 
			}
		}

		public MyLinksMenuControl() { }

		protected override void OnInit(EventArgs e)
		{
			if (!this.Visible)
				return;

			_WebUrl = SPContext.Current.Web.Url;

			try
			{
				if (SPContext.Current.Web.CurrentUser == null || Page.User == null || Page.User.Identity == null || !Page.User.Identity.IsAuthenticated)
				{
					this.Visible = false;
				}
				else if (UseMemberships || ShowQuickLinks)
				{
					UserProfileManager upm = new UserProfileManager(SPServiceContext.Current);
					_CurrentUser = upm.GetUserProfile(true);
				}
			}
			catch (Exception ex)
			{
				ShowQuickLinks = false; //Quick links won't work if the user's profile cannot be loaded.
				_CurrentUser = null;				
				_ErrorMessages.Add("User Error: " + ex.Message);
			}
		}

		protected override void Render(System.Web.UI.HtmlTextWriter output)
		{
			if (this.Visible && Page.Request.QueryString["ShowMyLinksLoadTime"] == "1")
			{
				output.Write("<div style=\"position:absolute;top:0px;left:0px;border:1px solid #BBB;background-color:white;font-size:8pt;\">");

				if (_LoadTimes.Count > 0)
				{
					foreach (KeyValuePair<string, long> loadTime in _LoadTimes)
					{
						long ms = loadTime.Value;
						string displayTime = ms + "ms";

						if (ms >= 60000)
							displayTime = (ms / 60000) + "mins";
						else if (ms > 1000)
							displayTime = (ms / 1000) + "secs";

						output.Write(loadTime.Key + ":" + displayTime + ", ");
					}

					output.Write("Used Cache: " + _LoadedSiteListFromCache);
				}
				else if (_CurrentUser == null)
					output.Write("<b style=\"color:red;\">My Links Error: User Profile service unavailable.</b>");
				else
					output.Write("<b style=\"color:red;\">My Links Error: No content could be loaded.</b>");

				output.Write(" Visible: " + this.Visible);

				foreach (string message in _ErrorMessages)
					output.Write("<br />" + HttpUtility.HtmlEncode(message));

				output.Write("</div>");
			}
			
			base.Render(output);
		}

		protected override void AddMenuItems()
		{
			if (!this.Visible)
				return;

			bool siteListAdded = false;
			Stopwatch sw = new Stopwatch();
			sw.Start();			

			try
			{
				siteListAdded = AddSiteList();						
			}
			catch (Exception ex)
			{
				_ErrorMessages.Add("Site List Error: " + ex.Message);
			}

			//Only add quick links if the user profile service is available.
			if (_CurrentUser != null && ShowQuickLinks)
			{
				if (siteListAdded)
					base.AddMenuItemSeparator();

				try
				{
					if (AddQuickLinks())
						base.AddMenuItemSeparator();
				}
				catch (Exception ex)
				{
					_ErrorMessages.Add("Quick Links Error: " + ex.Message);
				}

				AddManagementLinks();
			}

			sw.Stop();
			_LoadTimes.Add("Total", sw.ElapsedMilliseconds);
		}

		private void AddManagementLinks()
		{
			AddMenuItem(this.ID + "_AddToMyLinks", TEXT_ADD_TO_MY_LINKS, "/_layouts/images/ctoa32.png", "", "", "MyLinksMenu.addMylinks_Dialog('" + WebUrl + "');");
			AddMenuItem(this.ID + "_ManageMyLinks", TEXT_MANAGE_MY_LINKS, "/_layouts/images/ctom32.png", "", "", "MyLinksMenu.manageMylinks_Dialog('" + WebUrl + "');");
		}

		private bool AddQuickLinks()
		{
			if (_CurrentUser == null || !ShowQuickLinks)
				return false;
			
			Stopwatch sw = new Stopwatch();
			sw.Start();

			SortedList<string, List<QuickLink>> groups = new SortedList<string, List<QuickLink>>();
			QuickLinkManager quickLinkManager = _CurrentUser.QuickLinks;
			QuickLink[] quickLinks = quickLinkManager.GetItems();

			foreach (QuickLink quickLink in quickLinks.OrderBy(ql => ql.Title))
			{
				if (!groups.ContainsKey(quickLink.Group))
					groups.Add(quickLink.Group, new List<QuickLink>());

				groups[quickLink.Group].Add(quickLink);
			}

			if (groups.Count == 1)
			{
				//If there is only one group, then add all of the quick links to the main list.
				foreach (QuickLink quickLink in groups.ElementAt(0).Value)
				{
					AddMenuItem(this.ID + "_MyLink" + this.MenuTemplateControl.Controls.Count, quickLink.Title, "", "", quickLink.Url, null);
				}
			}
			//Create a sub-menu for each group.
			else if (groups.Count > 1)
			{
				foreach (KeyValuePair<string, List<QuickLink>> group in groups)
				{
					SubMenuTemplate groupMenu = new SubMenuTemplate();
					groupMenu.ID = this.ID + "_GroupMenu" + this.MenuTemplateControl.Controls.Count;
					groupMenu.Text = group.Key;

					foreach (QuickLink quickLink in group.Value)
					{
						MenuItemTemplate linkTemplate = new MenuItemTemplate(quickLink.Title);
						linkTemplate.ID = groupMenu.ID + "_MyLink" + groupMenu.Controls.Count;
						linkTemplate.ClientOnClickNavigateUrl = quickLink.Url;

						groupMenu.Controls.Add(linkTemplate);
					}

					this.MenuTemplateControl.Controls.Add(groupMenu);
				}
			}

			sw.Stop();
			_LoadTimes.Add("Quick Links", sw.ElapsedMilliseconds);

			return (groups.Count > 0);
		}

		private bool AddSiteList()
		{
			SortedList<string, SiteListItem> siteList = null;

			//If the ASP.NET SessionState hasn't been enabled, then Context.Session will be null.
			bool cacheEnabled = (CacheSiteList && Context.Session != null);

			if (cacheEnabled)
			{
				DateTime? lastRefresh = Page.Session[CACHE_ID + "_LastRefresh"] as DateTime?;
				long msSinceLastRefresh = (lastRefresh == null || Page.Request["ReloadMyLinks"] == "1" ? 0 : (int)DateTime.Now.Subtract((DateTime)lastRefresh).TotalMilliseconds);

				_LoadTimes.Add("Last Refresh", msSinceLastRefresh);

				if (Page.Request["ReloadMyLinks"] != "1" && msSinceLastRefresh < CacheDuration * 60000)
					siteList = Page.Session[CACHE_ID] as SortedList<string, SiteListItem>;	
			}

			if (siteList == null)
			{
				siteList = new SortedList<string, SiteListItem>();

				if (UseMemberships)
					GetMemberships(siteList);

				if (UseSiteSearch)
					GetSiteSearchResults(siteList);

				if (cacheEnabled)
				{
					Page.Session[CACHE_ID] = siteList;
					Page.Session[CACHE_ID + "_LastRefresh"] = DateTime.Now;
				}
			}
			else
			{
				_LoadedSiteListFromCache = true;
			}

			if (siteList.Count > 0)
			{
				List<SiteListItem> siteMap = BuildSiteMap(siteList);

				//Add root menu item.
				SubMenuTemplate sitesMenu = null;

				if (_CurrentUser != null && ShowQuickLinks)
				{
					sitesMenu = new SubMenuTemplate();
					sitesMenu.ID = this.ID + "_SitesMenu";
					sitesMenu.Text = TEXT_MY_SHAREPOINT_SITES;
					sitesMenu.Description = "";
					sitesMenu.ImageUrl = "/_layouts/images/MYSHRPTS.GIF";
					sitesMenu.MenuGroupId = MENUGROUPID;
				}

				foreach (SiteListItem site in siteMap.OrderBy(s => s.Title))
				{
					if (sitesMenu != null)
						AddSiteSubMenu(site, sitesMenu);
					else
						AddSiteSubMenu(site, this.MenuTemplateControl);
				}

				if (sitesMenu != null)
					this.MenuTemplateControl.Controls.Add(sitesMenu);

				return true;
			}
			else
				return false;
		}

		private void AddSiteSubMenu(SiteListItem site, Control parentMenu)
		{
			if (site.SubSites == null)
			{
				AddSiteToMenu(site, parentMenu);
			}
			else
			{
				SubMenuTemplate subSitesMenu = new SubMenuTemplate();
				subSitesMenu.ID = parentMenu.ID + "_SitesMenu" + parentMenu.Controls.Count;
				subSitesMenu.Text = ShortenTitle(site.Title);
				subSitesMenu.MenuGroupId = MENUGROUPID;

				//Since the SharePoint menu control does not allow links to be created at the root of a submenu, 
				//we'll add a "Root Site" link now and then use JavaScript to set up the link when the menu renders.
				AddSiteToMenu(new SiteListItem() { Url = site.Url, Title = TEXT_ROOT_SITE }, subSitesMenu);

				foreach (SiteListItem subSite in site.SubSites.OrderBy(s => s.Title))
				{
					AddSiteSubMenu(subSite, subSitesMenu);
				}

				parentMenu.Controls.Add(subSitesMenu);
			}
		}

		private void AddSiteToMenu(SiteListItem site, Control parentMenu)
		{
			MenuItemTemplate siteTemplate = new MenuItemTemplate(ShortenTitle(site.Title));
			siteTemplate.ID = parentMenu.ID + "_SiteLink" + parentMenu.Controls.Count;
			siteTemplate.ClientOnClickNavigateUrl = site.Url;
			siteTemplate.MenuGroupId = MENUGROUPID;
			parentMenu.Controls.Add(siteTemplate);
		}

		private void AddSiteToList(string url, string title, SortedList<string, SiteListItem> siteList)
		{
			SiteListItem site = new SiteListItem() { Url = url, Title = title };

			if (!siteList.ContainsKey(site.Url))
			{
				siteList.Add(site.Url, site);
			}
		}

		private void GetMemberships(SortedList<string, SiteListItem> siteList)
		{
			if (_CurrentUser == null || !UseMemberships)
				return;

			Stopwatch sw = new Stopwatch();
			sw.Start();

			//Load the site memberships from the user's profile.
			MembershipManager membershipManager = _CurrentUser.Memberships;

			foreach (Membership membership in membershipManager.GetItems())
			{
				if (membership.GroupType == MembershipGroupType.SharePointSite)
				{
					AddSiteToList(membership.Url, membership.Title, siteList);
				}
			}

			sw.Stop();
			_LoadTimes.Add("Memberships", sw.ElapsedMilliseconds);
		}

		private void GetSiteSearchResults(SortedList<string, SiteListItem> siteList)
		{
			Stopwatch sw = new Stopwatch();
			sw.Start();

			string queryText = "SELECT url, title " +
								"FROM Scope() " +
								"WHERE \"Scope\" = '" + SearchScope + "' " +
								"AND (ContentClass = 'STS_Site'" + (IncludeWebsInSearchResults ? " OR ContentClass = 'STS_Web'" : "") + ")";

			SearchServiceApplicationProxy proxy = (SearchServiceApplicationProxy)SearchServiceApplicationProxy.GetProxy(SPServiceContext.GetContext(SPContext.Current.Site));
			FullTextSqlQuery searchQuery = new FullTextSqlQuery(proxy);
			searchQuery.ResultsProvider = SearchProvider.Default;
			searchQuery.ResultTypes = ResultType.RelevantResults;
			searchQuery.EnableStemming = false;
			searchQuery.TrimDuplicates = true;
			searchQuery.QueryText = queryText;
			searchQuery.RowLimit = SearchResultLimit; 
			ResultTableCollection results = searchQuery.Execute();
			ResultTable result = results[ResultType.RelevantResults];

			while (result.Read())
			{
				AddSiteToList(result.GetString(0), result.GetString(1), siteList);
			}

			sw.Stop();
			_LoadTimes.Add("Site Search", sw.ElapsedMilliseconds);
		}

		/// <summary>
		/// Groups sites together based on their URL structure. 
		/// </summary>
		/// <param name="siteList">This list must be sorted by the URL!</param>
		private List<SiteListItem> BuildSiteMap(SortedList<string, SiteListItem> siteList)
		{
			Stopwatch sw = new Stopwatch();
			sw.Start();

			List<SiteListItem> siteMap = new List<SiteListItem>();

			foreach (SiteListItem site in siteList.Values)
			{
				//If a parent site wasn't found, then consider this site a parent.
				if (!AddSiteToParent(site, siteMap, 0))
					siteMap.Add(new SiteListItem() { Url=site.Url, Title = site.Title });
			}

			sw.Stop();
			_LoadTimes.Add("Site Map", sw.ElapsedMilliseconds);

			return siteMap;
		}

		/// <summary>
		/// Find the parent site using the specified site's URL and then add the specified site to the parent site's list.
		/// </summary>
		/// <returns>Whether or not a parent site was found.</returns>
		private bool AddSiteToParent(SiteListItem site, List<SiteListItem> siteList, int depth)
		{
			if (siteList.Count == 0)
				return false;

			SiteListItem lastParent = siteList[siteList.Count - 1];

			//If the URLs are the same, then report that the parent was found so that it doesn't get added again.
			if (String.Equals(site.Url, lastParent.Url, StringComparison.CurrentCultureIgnoreCase))
				return true;

			//If the current URL starts with the last parent's URL, then it's a child of that site.
			if (site.Url.StartsWith(lastParent.Url, StringComparison.CurrentCultureIgnoreCase))
			{
				if (depth < SiteMenuDepth && (lastParent.SubSites == null || !AddSiteToParent(site, lastParent.SubSites, depth + 1)))
				{
					lastParent.AddSubSite(new SiteListItem() { Url = site.Url, Title = site.Title });
				}

				return true;
			}

			return false;
		}
		
		private static string GetLocalizedString(string resourceName)
		{
			if (string.IsNullOrEmpty(resourceName))
				return string.Empty;

			return SPUtility.GetLocalizedString("$Resources:" + resourceName, "MyLinksMenu", (uint)CultureInfo.CurrentUICulture.LCID);
		}

		private string ShortenTitle(string title)
		{
			if (title.Length > MAX_TITLE_CHARS + TITLE_ELLIPSIS.Length)
				return title.Remove(MAX_TITLE_CHARS) + TITLE_ELLIPSIS;
			else
				return title;
		}
		
		public override MenuItemTemplate AddMenuItem(string id, string displayName, string imageUrl, string description, string navigateUrl, string onClickScript)
		{
			MenuItemTemplate template = base.AddMenuItem(id, displayName, imageUrl, description, navigateUrl, onClickScript);

			//Get rid of the default icon that's added.
			if (String.IsNullOrEmpty(imageUrl))
				template.ImageUrl = "";

			return template;
		}

		[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
		protected override void CreateChildControls()
		{
			base.CreateChildControls();

			if (!this.Visible)
				return;

			string scriptGuid = "71ad2dbb-db04-4323-b002-cffb64e57a98";
			string scriptKey = scriptGuid + "/MyLinksMenu.js";
			string variablesScriptKey = scriptGuid + "/Variables.js";

			if (!Page.ClientScript.IsClientScriptIncludeRegistered(scriptKey))
				Page.ClientScript.RegisterClientScriptInclude(scriptKey, WebUrl + "/_layouts/MyLinksMenu/MyLinksMenu.js");

			//This is needed to pass our JavaScript code the localized text needed for the dialogs along with other misc variables.
			if (!Page.ClientScript.IsClientScriptBlockRegistered(variablesScriptKey))
			{
				string variableScript = "var MyLinksMenu = MyLinksMenu || {};" +
										"MyLinksMenu.showQuickLinks = " + ShowQuickLinks.ToString().ToLower() + "; " +
										"MyLinksMenu.rootSiteText = \"" + TEXT_ROOT_SITE.Replace("\"", "\\\"") + "\"; " +
										"MyLinksMenu.addToMyLinksText = \"" + TEXT_ADD_TO_MY_LINKS.Replace("\"", "\\\"") + "\"; " +
										"MyLinksMenu.manageMyLinksText = \"" + TEXT_MANAGE_MY_LINKS.Replace("\"", "\\\"") + "\"; ";

				Page.ClientScript.RegisterClientScriptBlock(this.GetType(), variablesScriptKey, variableScript, true);
			}

			base.MenuControl.HoverCellActiveCssClass = "ms-SPLink ms-SpLinkButtonActive ms-welcomeMenu";
			base.MenuControl.HoverCellInActiveCssClass = "ms-SPLink ms-SpLinkButtonInActive ms-welcomeMenu";

			if (this.Web.UIVersion >= 4)
			{
				base.MenuControl.ArrowImageUrl = WebUrl +  "/_layouts/images/fgimg.png";
				base.MenuControl.ArrowImageWidth = 5;
				base.MenuControl.ArrowImageHeight = 3;
				base.MenuControl.ArrowImageOffsetX = 0;
				base.MenuControl.ArrowImageOffsetY = 0x1eb;
			}
			else
			{
				base.MenuControl.ArrowImageUrl = WebUrl + "/_layouts/images/menudark.gif";
			}
		}
	}
}
